package net.borlis.sample.service.entities;

import junit.framework.TestCase;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

/**
 * @author alex@borlis.net
 */
public abstract class BeanCodeChecker extends TestCase {

    private static final class TestObject {
    }

    private static final String[] TEST_STRING_VAL = new String[]{"value1", "value0"};
    private static final boolean[] TEST_BOOLEAN_VAL = new boolean[]{true, false};
    private static final short[] TEST_SHORT_VAL = new short[]{0, 1};
    private static final long[] TEST_LONG_VAL = new long[]{0, 1};
    private static final float[] TEST_FLOAT_VAL = new float[]{0, 1};
    private static final int[] TEST_INT_VAL = new int[]{0, 1};
    private static final byte[] TEST_BYTE_VAL = new byte[]{0, 1};
    private static final char[] TEST_CHAR_VAL = new char[]{0, 1};
    private static final double[] TEST_DOUBLE_VAL = new double[]{0, 1};
    private static final Date[] TEST_DATE_VAL = new Date[]{new Date(1000), new Date(2000)};
    private static final BigInteger[] TEST_BIG_INTEGER_VAL = new BigInteger[]{new BigInteger("0"), new BigInteger("1")};
    private static final BigDecimal[] TEST_BIG_DECIMAL_VAL = new BigDecimal[]{new BigDecimal("3"), new BigDecimal("5")};

    public static void assertImmutable(Class classUnderTest) {
        assertConstructors(classUnderTest);
    }


    public static void assertGetterSetter(Class classUnderTest) {
        assertGetterSetterContract(classUnderTest);
    }

    public static void assertMeetsContract(Class classUnderTest, String... usedFields) {
        assertMeetsEqualsContract(classUnderTest, usedFields);
        assertMeetsHashCodeContract(classUnderTest, usedFields);
        assertGetterSetterContract(classUnderTest);
    }

    private static Object justSavedValue;

    private static void assertConstructors(Class classUnderTest) {
    }

    private static void assertGetterSetterContract(Class classUnderTest) {
        try {
            Field[] fields = classUnderTest.getDeclaredFields();
            List<Field> classFields = new ArrayList<Field>(Arrays.asList(fields));

            for (Field field : classFields) {

                if (!Modifier.isStatic(field.getModifiers())) {
                    field.setAccessible(true);
                    Object o1 = classUnderTest.newInstance();
                    setFieldValue(o1, field, 1);
                    Method[] methods = classUnderTest.getMethods();
                    boolean found = false;
                    for (Method method : methods) {
                        final String getPrefix = "get";
                        if (method.getName().toLowerCase().equals(getPrefix + field.getName().toLowerCase())) {
                            Object returnObject = method.invoke(o1);
                            System.out.println(classUnderTest.getSimpleName() + " - " + method.getName() + " - " + returnObject);
                            if (!returnObject.equals(getValue(field, 1))) {
                                if (!returnObject.equals(justSavedValue)) {
                                    if (!(returnObject.getClass() == new byte[]{}.getClass() &&
                                            Arrays.equals((byte[]) returnObject, (byte[]) getValue(field, 1)))) {
                                        fail("getter must return the received value");
                                    }
                                }
                            }
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        throw new IllegalStateException("there is no getter for the field " + field.getName());
                    }
                }

            }

        } catch (InstantiationException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to invoke method for an instance of the class under test");
        } catch (IllegalStateException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to invoke method for an instance of the class under test");
        }

    }

    private static Object getValue(Field field, int index) throws IllegalAccessException, InstantiationException {
        if (field.getType() == String.class) {
            return TEST_STRING_VAL[index];

        } else if (field.getType() == boolean.class) {
            return TEST_BOOLEAN_VAL[index];

        } else if (field.getType() == short.class) {
            return TEST_SHORT_VAL[index];

        } else if (field.getType() == long.class) {
            return TEST_LONG_VAL[index];

        } else if (field.getType() == float.class) {
            return TEST_FLOAT_VAL[index];

        } else if (field.getType() == int.class) {
            return TEST_INT_VAL[index];

        } else if (field.getType() == byte.class) {
            return TEST_BYTE_VAL[index];

        } else if (field.getType() == char.class) {
            return TEST_CHAR_VAL[index];

        } else if (field.getType() == double.class) {
            return TEST_DOUBLE_VAL[index];

        } else if (field.getType() == Long.class) {
            return TEST_LONG_VAL[index];

        } else if (field.getType() == BigDecimal.class) {
            return TEST_BIG_DECIMAL_VAL[index];

        } else if (field.getType() == Integer.class) {
            return TEST_INT_VAL[index];

        } else if (field.getType() == Byte.class) {
            return TEST_BYTE_VAL[index];

        } else if (field.getType() == Boolean.class) {
            return TEST_BOOLEAN_VAL[index];

        } else if (field.getType() == Date.class) {
            return TEST_DATE_VAL[index];

        } else if (field.getType() == BigInteger.class) {
            return TEST_BIG_INTEGER_VAL[index];
        } else if (field.getType().isEnum()) {
            return field.getType().getEnumConstants()[index]; // <- expected that your enum has 2 items at least

        } else if (field.getType() == Set.class) {
            return Collections.emptySet();

        } else if (field.getType() == List.class) {
            return Collections.emptyList();

        } else if (field.getType() == new byte[]{}.getClass()) {
            return new byte[]{};

        } else if (Object.class.isAssignableFrom(field.getType())) {
            Object res = field.getType().newInstance();
            Field[] paramFields = res.getClass().getDeclaredFields();
            for (Field paramField : paramFields) {
                if (!Modifier.isStatic(paramField.getModifiers())) {
                    paramField.setAccessible(true);
                    if (paramField.getType() == String.class) {
                        paramField.set(res, TEST_STRING_VAL[index]);
                    }
                    if (paramField.getType() == BigInteger.class) {
                        paramField.set(res, TEST_BIG_INTEGER_VAL[index]);
                    }
                    if (paramField.getType() == BigDecimal.class) {
                        paramField.set(res, TEST_BIG_DECIMAL_VAL[index]);
                    }
                    if (paramField.getType() == Long.class) {
                        paramField.set(res, TEST_LONG_VAL[index]);// <- for classes without string fields
                    }
                }
            }
            return res;
        }
        throw new IllegalArgumentException("Don't know how to set a " + field.getType().getName());
    }

    private static void setFieldValue(Object object, Field field, int index) throws IllegalAccessException, InstantiationException, InvocationTargetException {
        if (!Modifier.isStatic(field.getModifiers())) {

            Method[] methods = object.getClass().getMethods();
            boolean found = false;
            for (Method method : methods) {

                final String setPrefix = "set";
                if (method.getName().toLowerCase().equals(setPrefix + field.getName().toLowerCase())) {
                    System.out.println(object.getClass().getSimpleName() + " - " + method.getName());
                    found = true;

                    if (field.getType() == String.class) {
//                        field.set(object, TEST_STRING_VAL[index]);
                        method.invoke(object, TEST_STRING_VAL[index]);

                    } else if (field.getType() == boolean.class) {
//                        field.setBoolean(object, TEST_BOOLEAN_VAL[index]);
                        method.invoke(object, TEST_BOOLEAN_VAL[index]);

                    } else if (field.getType() == short.class) {
//                        field.setShort(object, TEST_SHORT_VAL[index]);
                        method.invoke(object, TEST_SHORT_VAL[index]);

                    } else if (field.getType() == long.class) {
//                        field.setLong(object, TEST_LONG_VAL[index]);
                        method.invoke(object, TEST_LONG_VAL[index]);

                    } else if (field.getType() == BigDecimal.class) {
//                        field.setLong(object, TEST_LONG_VAL[index]);
                        method.invoke(object, TEST_BIG_DECIMAL_VAL[index]);

                    } else if (field.getType() == float.class) {
//                        field.setFloat(object, TEST_FLOAT_VAL[index]);
                        method.invoke(object, TEST_FLOAT_VAL[index]);

                    } else if (field.getType() == int.class) {
//                        field.setInt(object, TEST_INT_VAL[index]);
                        method.invoke(object, TEST_INT_VAL[index]);

                    } else if (field.getType() == byte.class) {
//                        field.setByte(object, TEST_BYTE_VAL[index]);
                        method.invoke(object, TEST_BYTE_VAL[index]);

                    } else if (field.getType() == char.class) {
//                        field.setChar(object, TEST_CHAR_VAL[index]);
                        method.invoke(object, TEST_CHAR_VAL[index]);

                    } else if (field.getType() == double.class) {
//                        field.setDouble(object, TEST_DOUBLE_VAL[index]);
                        method.invoke(object, TEST_DOUBLE_VAL[index]);

                    } else if (field.getType() == Long.class) {
//                        field.set(object, TEST_LONG_VAL[index]);
                        method.invoke(object, TEST_LONG_VAL[index]);

                    } else if (field.getType() == Integer.class) {
//                        field.set(object, TEST_INT_VAL[index]);
                        method.invoke(object, TEST_INT_VAL[index]);

                    } else if (field.getType() == Byte.class) {
//                        field.set(object, TEST_BYTE_VAL[index]);
                        method.invoke(object, TEST_BYTE_VAL[index]);

                    } else if (field.getType() == BigInteger.class) {
//                        field.set(object, TEST_BYTE_VAL[index]);
                        method.invoke(object, TEST_BIG_INTEGER_VAL[index]);

                    } else if (field.getType() == Byte.class) {
//                        field.set(object, TEST_BYTE_VAL[index]);
                    method.invoke(object, TEST_BYTE_VAL[index]);

                    } else if (field.getType() == Boolean.class) {
//                        field.set(object, TEST_BOOLEAN_VAL[index]);
                    method.invoke(object, TEST_BOOLEAN_VAL[index]);

                    } else if (field.getType() == Date.class) {
//                        field.set(object, TEST_DATE_VAL[index]);
                    method.invoke(object, TEST_DATE_VAL[index]);

                    } else if (field.getType().isEnum()) {
//                        field.set(object, field.getType().getEnumConstants()[index]); // <- expected that your enum has 2 items at least
                    method.invoke(object, field.getType().getEnumConstants()[index]);

                    } else if (field.getType() == Set.class) {
//                        field.set(object, Collections.emptySet());
                    method.invoke(object, Collections.emptySet());

                    } else if (field.getType() == List.class) {
//                        field.set(object, Collections.emptyList());
                    method.invoke(object, Collections.emptyList());

                    } else if (field.getType() == new byte[]{}.getClass()) {
//                        field.set(object, Collections.emptySet());
                    byte[] b = new byte[]{};
                    method.invoke(object, b);

                } else if (Object.class.isAssignableFrom(field.getType())) {
//                        field.set(object, field.getType().newInstance());
                    method.invoke(object, field.getType().newInstance());
                    //Even though we have different instances, they are equal. Let's walk one of them
                    //to see if we can find a field to set
                    Field[] paramFields = field.get(object).getClass().getDeclaredFields();
                    for (Field paramField : paramFields) {
                        if (!Modifier.isStatic(paramField.getModifiers())) {
                            paramField.setAccessible(true);
                            if (paramField.getType() == String.class) {
                                paramField.set(field.get(object), TEST_STRING_VAL[index]);
                            }
                            if (paramField.getType() == BigInteger.class) {
                                paramField.set(field.get(object), TEST_BIG_INTEGER_VAL[index]);
                            }

                            if (paramField.getType() == BigDecimal.class) {
                                paramField.set(field.get(object), TEST_BIG_DECIMAL_VAL[index]);
                            }

                            if (paramField.getType() == Long.class) {
                                paramField.set(field.get(object), TEST_LONG_VAL[index]);// <- for classes without string fields
                            }
                        }
                    }
                    justSavedValue = field.get(object);
                } else {
                    fail("Don't know how to set a " + field.getType().getName());
                }
                break;
            }
        }
        if (!found) {
            throw new IllegalStateException("there is no setter for the field " + field.getName());
        }
    }

}

    private static void assertMeetsEqualsContract(Class classUnderTest, String... usedFields) {

        Object o1;
        Object o2;
        try {
            o1 = classUnderTest.newInstance();
            o2 = classUnderTest.newInstance();
            assertTrue("Instances with default constructor not equal (o1.equals(o2))", o1.equals(o2));
            assertTrue("Instances with default constructor not equal (o2.equals(o1))", o2.equals(o1));
            TestObject testObject = new TestObject(); //test object to check that equals follows that case
            assertFalse("Instances of diff classes must be NOT equals", o1.equals(testObject));
            assertFalse("Instances of diff classes must be NOT equals", testObject.equals(o1));

            Field[] fields = classUnderTest.getDeclaredFields();
            List<String> providedFields = new ArrayList<String>(Arrays.asList(usedFields));
            List<Field> classFields = new ArrayList<Field>(Arrays.asList(fields));
            checkProvidedFields(providedFields, classFields);

            for (Field field : classFields) {
                if (providedFields.contains(field.getName())) {
                    field.setAccessible(true);
                    //Reset the instances
                    o1 = classUnderTest.newInstance();
                    o2 = classUnderTest.newInstance();

                    setFieldValue(o1, field, 0);
                    assertFalse("Instances with o1 having " + field.getName() + " set and o2 having it not set are equal", o1.equals(o2));

                    field.set(o2, field.get(o1));
                    assertTrue("After setting o2 with the value of the object in o1, the two objects in the field are not equal"
                            , field.get(o1).equals(field.get(o2)));

                    assertTrue("Instances with o1 having "
                            + field.getName()
                            + " set and o2 having it set to the same object of type "
                            + field.get(o2).getClass().getName()
                            + " are not equal", o1.equals(o2));

                    setFieldValue(o2, field, 1);

                    assertFalse("After setting o2 with a different object than what is in o1, the two objects in the field are equal. "
                            + "This is after an attempt to walk the fields to make them different"
                            , field.get(o1).equals(field.get(o2)));
                    assertFalse("Instances with o1 having " + field.getName() + " set and o2 having it set to a different object are equal", o1.equals(o2));
                }
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        }
    }

    private static void assertMeetsHashCodeContract(Class classUnderTest, String... usedFields) {

        try {
            Field[] fields = classUnderTest.getDeclaredFields();
            List<String> providedFields = new ArrayList<String>(Arrays.asList(usedFields));
            List<Field> classFields = new ArrayList<Field>(Arrays.asList(fields));
            checkProvidedFields(providedFields, classFields);

            for (Field field : classFields) {
                if (providedFields.contains(field.getName())) {
                    field.setAccessible(true);
                    Object o1 = classUnderTest.newInstance();
                    int initialHashCode = o1.hashCode();
                    setFieldValue(o1, field, 1);
                    int updatedHashCode = o1.hashCode();
                    assertFalse("The field " + field.getName() + " was not taken into account for the hashCode contract ", initialHashCode == updatedHashCode);
                }
            }

        } catch (InstantiationException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            throw new AssertionError("Unable to construct an instance of the class under test");
        }
    }

    private static void checkProvidedFields(List<String> providedFields, List<Field> classFields) {
        boolean res;
        for (String providedField : providedFields) {
            res = false;
            for (Field classField : classFields) {
                if (classField.getName().equals(providedField)) {
                    res = true;
                    break;
                }
            }
            if (!res) {
                fail("check parameters: " + providedField + " does not exist");
            }
        }
    }

}